const Promise = require("bluebird");
const { remove, assign } = require("lodash");

const DEFAULT_LIMIT = 10;
const DEFAULT_OFFSET = 0;
const DEFAULT_PAGE = 1;

/**
 * Exports a plugin to pass into the bookshelf instance, i.e.:
 *
 *      import config from './knexfile';
 *      import knex from 'knex';
 *      import bookshelf from 'bookshelf';
 *
 *      const ORM = bookshelf(knex(config));
 *
 *      ORM.plugin('bookshelf-pagination-plugin');
 *
 *      export default ORM;
 *
 * The plugin attaches two instance methods to the bookshelf
 * Model object: orderBy and fetchPage.
 *
 * Model#orderBy calls the underlying query builder's orderBy method, and
 * is useful for ordering the paginated results.
 *
 * Model#fetchPage works like Model#fetchAll, but returns a single page of
 * results instead of all results, as well as the pagination information
 *
 * See methods below for details.
 */
module.exports = function paginationPlugin(bookshelf) {
    /**
     * @method Model#fetchPage
     * @belongsTo Model
     *
     * Similar to {@link Model#fetchAll}, but fetches a single page of results
     * as specified by the limit (page size) and offset or page number.
     *
     * Any options that may be passed to {@link Model#fetchAll} may also be passed
     * in the options to this method.
     *
     * To perform pagination, you may include *either* an `offset` and `limit`, **or**
     * a `page` and `pageSize`.
     *
     * By default, with no parameters or missing parameters, `fetchPage` will use an
     * options object of `{page: 1, pageSize: 10}`
     *
     *
     * Below is an example showing the user of a JOIN query with sort/ordering,
     * pagination, and related models.
     *
     * @example
     *
     * Car
     * .query(function (qb) {
     *    qb.innerJoin('manufacturers', 'cars.manufacturer_id', 'manufacturers.id');
     *    qb.groupBy('cars.id');
     *    qb.where('manufacturers.country', '=', 'Sweden');
     * })
     * .orderBy('-productionYear') // Same as .orderBy('cars.productionYear', 'DESC')
     * .fetchPage({
     *    pageSize: 15, // Defaults to 10 if not specified
     *    page: 3, // Defaults to 1 if not specified
     *
     *    // OR
     *    // limit: 15,
     *    // offset: 30,
     *
     *    withRelated: ['engine'] // Passed to Model#fetchAll
     * })
     * .then(function (results) {
     *    console.log(results); // Paginated results object with metadata example below
     * })
     *
     * // Pagination results:
     *
     * {
     *    models: [<Car>], // Regular bookshelf Collection
     *    // other standard Collection attributes
     *    ...
     *    pagination: {
     *        rowCount: 53, // Total number of rows found for the query before pagination
     *        pageCount: 4, // Total number of pages of results
     *        page: 3, // The requested page number
     *        pageSze: 15, // The requested number of rows per page
     *
     *  // OR, if limit/offset pagination is used instead of page/pageSize:
     *        // offset: 30, // The requested offset
     *        // limit: 15 // The requested limit
     *    }
     * }
     *
     * @param options {object}
     *    The pagination options, plus any additional options that will be passed to
     *    {@link Model#fetchAll}
     * @returns {Promise<Model|null>}
     */
    function fetchPage(options = {}) {
        const { page, pageSize, limit, offset } = options;

        delete options.page;
        delete options.pageSize;
        delete options.limit;
        delete options.offset;

        const fetchOptions = options;

        let usingPageSize = false; // usingPageSize = false means offset/limit, true means page/pageSize
        let _page;
        let _pageSize;
        let _limit;
        let _offset;

        function ensureIntWithDefault(val, def) {
            if (!val) {
                return def;
            }

            const _val = parseInt(val);
            if (Number.isNaN(_val)) {
                return def;
            }

            return _val;
        }

        if (!limit && !offset) {
            usingPageSize = true;

            _pageSize = ensureIntWithDefault(pageSize, DEFAULT_LIMIT);
            _page = ensureIntWithDefault(page, DEFAULT_PAGE);

            _limit = _pageSize;
            _offset = _limit * (_page - 1);
        } else {
            _pageSize = _limit; // not used, just for eslint `const` error
            _limit = ensureIntWithDefault(limit, DEFAULT_LIMIT);
            _offset = ensureIntWithDefault(offset, DEFAULT_OFFSET);
        }

        const tableName = this.constructor.prototype.tableName;
        const idAttribute = this.constructor.prototype.idAttribute
            ? this.constructor.prototype.idAttribute
            : "id";

        const paginate = () => {
            // const pageQuery = clone(this.query());
            const pager = this.constructor.forge();

            return pager
                .query(qb => {
                    assign(qb, this.query().clone());
                    qb.limit.apply(qb, [_limit]);
                    qb.offset.apply(qb, [_offset]);
                    return null;
                })
                ._fetchAll(fetchOptions);
        };

        const count = () => {
            const notNeededQueries = [
                "orderByBasic",
                "orderByRaw",
                "groupByBasic",
                "groupByRaw"
            ];
            const counter = this.constructor.forge();

            return counter
                .query(qb => {
                    assign(qb, this.query().clone());

                    // Remove grouping and ordering. Ordering is unnecessary
                    // for a count, and grouping returns the entire result set
                    // What we want instead is to use `DISTINCT`
                    remove(qb._statements, statement => {
                        return (
                            notNeededQueries.indexOf(statement.type) > -1 ||
                            statement.grouping === "columns"
                        );
                    });
                    qb.countDistinct.apply(qb, [`${tableName}.${idAttribute}`]);
                })
                ._fetchAll()
                .then(result => {
                    const metadata = usingPageSize
                        ? { page: _page, pageSize: _limit }
                        : { offset: _offset, limit: _limit };

                    if (result && result.length === 1) {
                        // We shouldn't have to do this, instead it should be
                        // result.models[0].get('count')
                        // but SQLite uses a really strange key name.
                        const count = result.models[0];
                        const keys = Object.keys(count.attributes);
                        if (keys.length === 1) {
                            const key = Object.keys(count.attributes)[0];
                            metadata.rowCount = parseInt(count.attributes[key]);
                        }
                    }

                    return metadata;
                });
        };

        return Promise.join(paginate(), count()).then(([rows, metadata]) => {
            const pageCount = Math.ceil(metadata.rowCount / _limit);
            const pageData = assign(metadata, { pageCount });
            return assign(rows, { pagination: pageData });
        });
    }

    bookshelf.Model.prototype.fetchPage = fetchPage;

    bookshelf.Model.fetchPage = function(...args) {
        return this.forge().fetchPage(...args);
    };

    bookshelf.Collection.prototype.fetchPage = function(...args) {
        return fetchPage.apply(this.model.forge(), args);
    };
};
